package com.xingchi.tornado.log.aspect;

import com.alibaba.fastjson.serializer.ValueFilter;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.xingchi.tornado.core.context.SpringContextHolder;
import com.xingchi.tornado.core.utils.RequestUtils;
import com.xingchi.tornado.log.aspect.anno.OperateLog;
import com.xingchi.tornado.log.event.OperateLogEvent;
import com.xingchi.tornado.log.model.dto.OperateLogDTO;
import com.xingchi.tornado.log.utils.DataMaskingUtils;
import com.xingchi.tornado.security.context.RequestContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.StringJoiner;

import static com.xingchi.tornado.log.aspect.WebLogAspect.ignoreArgs;

/**
 * 业务日志记录切面
 *
 * @author xiaoya
 * @date 2024/9/26 11:17
 */
@Aspect
public class BizLogAspect {

    private static final Logger log = LoggerFactory.getLogger(BizLogAspect.class);
    private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();

    @Pointcut("@annotation(com.xingchi.tornado.log.aspect.anno.OperateLog)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        OperateLog operateLogAnno = method.getAnnotation(OperateLog.class);

        // 获取模块名和操作名
        String moduleName = obtainModuleName(signature);
        String operate = obtainOperation(signature);

        if (!StringUtils.hasText(moduleName) && !StringUtils.hasText(operate)) {
            // 两个都没值不记录，直接进行返回
            return joinPoint.proceed();
        }

        // 构建审计日志对象
        OperateLogDTO operateLogDTO = new OperateLogDTO();
        operateLogDTO.setModuleName(moduleName);
        operateLogDTO.setOperate(operate);
        operateLogDTO.setLevel(operateLogAnno.level());

        // 设置标签信息
        if (operateLogAnno.tags().length > 0) {
            StringJoiner joiner = new StringJoiner(",");
            for (String tag : operateLogAnno.tags()) {
                joiner.add(tag);
            }
            operateLogDTO.setTags(joiner.toString());
        }

        // 获取请求信息
        HttpServletRequest request = RequestUtils.getRequest();
        String uri = request.getRequestURI();
        String ip = RequestUtils.getIp();
        String typeName = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();
        String operateMethodName = typeName + "#" + methodName;

        operateLogDTO.setUri(uri);
        operateLogDTO.setIp(ip);
        operateLogDTO.setOperateMethod(operateMethodName);
        operateLogDTO.setCreateTime(LocalDateTime.now());

        // 获取用户信息
        try {
            operateLogDTO.setOperatorId(RequestContext.getUserId());
            operateLogDTO.setOperatorName(RequestContext.getName());
            operateLogDTO.setOperatorUsername(RequestContext.getUsername());
            operateLogDTO.setSessionId(RequestContext.getSessionId());
            operateLogDTO.setUserAgent(RequestContext.getUserAgent());
        } catch (Exception e) {
            log.debug("获取用户信息失败: {}", e.getMessage());
        }

        // 设置请求参数，根据类型分类
        if (operateLogAnno.logRequest()) {
            Object[] args = joinPoint.getArgs();
            String[] parameterNames = Optional.ofNullable(PARAMETER_NAME_DISCOVERER.getParameterNames(method)).orElse(new String[0]);
            Parameter[] parameters = method.getParameters();

            // 分类存储请求参数
            Map<String, Object> queryParams = new HashMap<>();
            Map<String, Object> pathParams = new HashMap<>();
            Map<String, Object> bodyParams = new HashMap<>();

            try {
                for (int i = 0; i < parameterNames.length; i++) {
                    String parameterName = parameterNames[i];
                    Object parameterValue = args[i];

                    // 跳过忽略的参数类型
                    if (parameterValue == null || ignoreArgs(parameterValue) ||
                            containsField(operateLogAnno.excludeParams(), parameterName)) {
                        continue;
                    }

                    // 获取参数上的注解
                    Annotation[] annotations = parameters[i].getAnnotations();
                    boolean categorized = false;

                    for (Annotation annotation : annotations) {
                        if (annotation instanceof RequestParam) {
                            RequestParam requestParam = (RequestParam) annotation;
                            String name = requestParam.value().isEmpty() ? parameterName : requestParam.value();
                            queryParams.put(name, parameterValue);
                            categorized = true;
                        } else if (annotation instanceof PathVariable) {
                            PathVariable pathVariable = (PathVariable) annotation;
                            String name = pathVariable.value().isEmpty() ? parameterName : pathVariable.value();
                            pathParams.put(name, parameterValue);
                            categorized = true;
                        } else if (annotation instanceof RequestBody) {
                            bodyParams.put(parameterName, parameterValue);
                            categorized = true;
                        }
                    }

                    // 如果没有归类，则默认作为query参数
                    if (!categorized) {
                        queryParams.put(parameterName, parameterValue);
                    }
                }

                // 转换为JSON并进行脱敏处理
                ValueFilter filter = (object, name, value) -> value != null && ignoreArgs(value) ? String.format("字段[%s]类型[%s]类无法转JSON", name, value.getClass().getSimpleName()) : value;
                String queryParamsJson = JSON.toJSONString(queryParams, filter, JSONWriter.Feature.WriteMapNullValue);
                String pathParamsJson = JSON.toJSONString(pathParams);
                String bodyParamsJson = JSON.toJSONString(bodyParams);

                if (operateLogAnno.desensitize()) {
                    queryParamsJson = DataMaskingUtils.maskJsonString(queryParamsJson, operateLogAnno.excludeParams());
                    pathParamsJson = DataMaskingUtils.maskJsonString(pathParamsJson, operateLogAnno.excludeParams());
                    bodyParamsJson = DataMaskingUtils.maskJsonString(bodyParamsJson, operateLogAnno.excludeParams());
                }

                operateLogDTO.setQueryParameter(queryParamsJson);
                operateLogDTO.setPathParameter(pathParamsJson);
                operateLogDTO.setBodyParameter(bodyParamsJson);

                // 兼容之前的字段
                Map<String, Object> allParams = new HashMap<>();
                allParams.putAll(queryParams);
                allParams.putAll(pathParams);
                allParams.putAll(bodyParams);
                String requestParameter = operateLogAnno.desensitize() ?
                        DataMaskingUtils.maskJsonString(JSONObject.toJSONString(allParams), operateLogAnno.excludeParams()) :
                        JSONObject.toJSONString(allParams);
                operateLogDTO.setRequestParameter(requestParameter);
            } catch (Exception e) {
                log.warn("请求参数记录失败，原因：{}", e.getMessage(), e);
            }
        }

        // 执行原方法
        long startTime = System.currentTimeMillis();
        Object proceed = null;
        boolean isSuccess = true;
        Throwable ex = null;
        try {
            proceed = joinPoint.proceed();
        } catch (Throwable e) {
            isSuccess = false;
            ex = e;
        }
        long endTime = System.currentTimeMillis();
        operateLogDTO.setExecutionTime(endTime - startTime);

        // 记录响应结果
        if (operateLogAnno.logResponse()) {
            try {
                if (isSuccess) {
                    operateLogDTO.setSuccess(0);
                    if (proceed != null) {
                        String responseParameter = JSONObject.toJSONString(proceed);
                        if (operateLogAnno.desensitize()) {
                            responseParameter = DataMaskingUtils.maskJsonString(responseParameter, operateLogAnno.excludeParams());
                        }
                        operateLogDTO.setResponseParameter(responseParameter);
                    } else {
                        operateLogDTO.setResponseParameter("null");
                    }
                } else {
                    operateLogDTO.setSuccess(1);
                    StringBuilder sb = new StringBuilder();
                    sb.append(ex.getMessage()).append("\n");
                    StackTraceElement[] stackTrace = ex.getStackTrace();
                    for (int i = 0; i < Math.min(stackTrace.length, 10); i++) { // 只记录前10行堆栈
                        sb.append(stackTrace[i].toString()).append("\n");
                    }
                    operateLogDTO.setResponseParameter(sb.toString());
                }
            } catch (Exception e) {
                log.warn("响应参数记录失败，原因：{}", e.getMessage(), e);
            }
        }

        try {
            // 发布日志记录事件
            SpringContextHolder.publishAsyncEvent(new OperateLogEvent(operateLogDTO), null);
        } catch (Exception e) {
            log.warn("日志发布失败，原因：{}", e.getMessage(), e);
        }

        if (!isSuccess) {
            throw ex;
        }

        return proceed;
    }

    /**
     * 判断字段是否在排除列表中
     *
     * @param excludeFields 排除字段列表
     * @param fieldName 字段名
     * @return 是否包含
     */
    private boolean containsField(String[] excludeFields, String fieldName) {
        if (excludeFields == null || fieldName == null) {
            return false;
        }
        for (String field : excludeFields) {
            if (field.equals(fieldName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取模块名称
     *
     * @param signature 方法签名
     * @return 模块名称
     */
    public String obtainModuleName(MethodSignature signature) {
        Method method = signature.getMethod();
        OperateLog annotation = method.getAnnotation(OperateLog.class);
        if (StringUtils.hasText(annotation.module())) {
            return annotation.module();
        }

        Class<?> declaringType = signature.getDeclaringType();
        // 都为空并且类上或接口上存在 OperateLog 则获取Api接口的
        Tag tag = declaringType.getAnnotation(Tag.class);
        return tag != null ? tag.name() : "";
    }

    /**
     * 获取操作名称
     *
     * @param signature 方法签名
     * @return 操作名称
     */
    public String obtainOperation(MethodSignature signature) {
        Method method = signature.getMethod();
        OperateLog annotation = method.getAnnotation(OperateLog.class);
        if (StringUtils.hasText(annotation.operate())) {
            return annotation.operate();
        }

        // 如果没有 注解中没有包含operation 那么取api接口中的信息
        Operation operation = method.getAnnotation(Operation.class);
        if (operation != null && StringUtils.hasText(operation.summary())) {
            return operation.summary();
        }
        return "";
    }
}
